/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hsc.lock.config;

import com.hsc.lock.interceptor.LockInterceptor;
import com.hsc.lock.interceptor.LockPutOperation;
import com.hsc.lock.interceptor.NameMatchLockOperationSource;
import com.hsc.lock.interceptor.ReplayLockOperation;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.parsing.ReaderContext;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * {@link org.springframework.beans.factory.xml.BeanDefinitionParser
 * BeanDefinitionParser} for the {@code <tx:advice/>} tag.
 *
 * @author Costin Leau
 * @author Phillip Webb
 * @author Stephane Nicoll
 */
class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {

    private static final String CACHEABLE_ELEMENT = "cacheable";

    private static final String CACHE_EVICT_ELEMENT = "cache-evict";

    private static final String CACHE_PUT_ELEMENT = "cache-put";

    private static final String METHOD_ATTRIBUTE = "method";

    private static final String DEFS_ELEMENT = "caching";


    @Override
    protected Class<?> getBeanClass(Element element) {
        return LockInterceptor.class;
    }

    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        builder.addPropertyReference("cacheManager", CacheNamespaceHandler.extractCacheManager(element));
        CacheNamespaceHandler.parseKeyGenerator(element, builder.getBeanDefinition());

        List<Element> cacheDefs = DomUtils.getChildElementsByTagName(element, DEFS_ELEMENT);
        if (!cacheDefs.isEmpty()) {
            // Using attributes source.
            List<RootBeanDefinition> attributeSourceDefinitions = parseDefinitionsSources(cacheDefs, parserContext);
            builder.addPropertyValue("cacheOperationSources", attributeSourceDefinitions);
        } else {
            // Assume annotations source.
            builder.addPropertyValue("cacheOperationSources",
                    new RootBeanDefinition("org.springframework.cache.annotation.AnnotationCacheOperationSource"));
        }
    }

    private List<RootBeanDefinition> parseDefinitionsSources(List<Element> definitions, ParserContext parserContext) {
        ManagedList<RootBeanDefinition> defs = new ManagedList<>(definitions.size());

        // extract default param for the definition
        for (Element element : definitions) {
            defs.add(parseDefinitionSource(element, parserContext));
        }

        return defs;
    }

    private RootBeanDefinition parseDefinitionSource(Element definition, ParserContext parserContext) {
        Props prop = new Props(definition);
        // add cacheable first

        ManagedMap<TypedStringValue, Collection<ReplayLockOperation>> cacheOpMap = new ManagedMap<>();
        cacheOpMap.setSource(parserContext.extractSource(definition));

        List<Element> cacheableCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHEABLE_ELEMENT);

        for (Element opElement : cacheableCacheMethods) {
            String name = prop.merge(opElement, parserContext.getReaderContext());
            TypedStringValue nameHolder = new TypedStringValue(name);
            nameHolder.setSource(parserContext.extractSource(opElement));
            CacheableOperation.Builder builder = prop.merge(opElement,
                    parserContext.getReaderContext(), new CacheableOperation.Builder());
            builder.setUnless(getAttributeValue(opElement, "unless", ""));
            builder.setSync(Boolean.parseBoolean(getAttributeValue(opElement, "sync", "false")));

            Collection<ReplayLockOperation> col = cacheOpMap.get(nameHolder);
            if (col == null) {
                col = new ArrayList<>(2);
                cacheOpMap.put(nameHolder, col);
            }
            col.add(builder.build());
        }

        List<Element> evictCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_EVICT_ELEMENT);

        for (Element opElement : evictCacheMethods) {
            String name = prop.merge(opElement, parserContext.getReaderContext());
            TypedStringValue nameHolder = new TypedStringValue(name);
            nameHolder.setSource(parserContext.extractSource(opElement));
            CacheEvictOperation.Builder builder = prop.merge(opElement,
                    parserContext.getReaderContext(), new CacheEvictOperation.Builder());

            String wide = opElement.getAttribute("all-entries");
            if (StringUtils.hasText(wide)) {
                builder.setCacheWide(Boolean.parseBoolean(wide.trim()));
            }

            String after = opElement.getAttribute("before-invocation");
            if (StringUtils.hasText(after)) {
                builder.setBeforeInvocation(Boolean.parseBoolean(after.trim()));
            }

            Collection<ReplayLockOperation> col = cacheOpMap.get(nameHolder);
            if (col == null) {
                col = new ArrayList<>(2);
                cacheOpMap.put(nameHolder, col);
            }
            col.add(builder.build());
        }

        List<Element> putCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_PUT_ELEMENT);

        for (Element opElement : putCacheMethods) {
            String name = prop.merge(opElement, parserContext.getReaderContext());
            TypedStringValue nameHolder = new TypedStringValue(name);
            nameHolder.setSource(parserContext.extractSource(opElement));
            LockPutOperation.Builder builder = prop.merge(opElement,
                    parserContext.getReaderContext(), new LockPutOperation.Builder());
            builder.setUnless(getAttributeValue(opElement, "unless", ""));

            Collection<ReplayLockOperation> col = cacheOpMap.get(nameHolder);
            if (col == null) {
                col = new ArrayList<>(2);
                cacheOpMap.put(nameHolder, col);
            }
            col.add(builder.build());
        }

        RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchLockOperationSource.class);
        attributeSourceDefinition.setSource(parserContext.extractSource(definition));
        attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpMap);
        return attributeSourceDefinition;
    }


    private static String getAttributeValue(Element element, String attributeName, String defaultValue) {
        String attribute = element.getAttribute(attributeName);
        if (StringUtils.hasText(attribute)) {
            return attribute.trim();
        }
        return defaultValue;
    }


    /**
     * Simple, reusable class used for overriding defaults.
     */
    private static class Props {

        private String key;

        private String keyGenerator;

        private String cacheManager;

        private String condition;

        private String method;

        @Nullable
        private String[] caches;

        Props(Element root) {
            String defaultCache = root.getAttribute("cache");
            this.key = root.getAttribute("key");
            this.keyGenerator = root.getAttribute("key-generator");
            this.cacheManager = root.getAttribute("cache-manager");
            this.condition = root.getAttribute("condition");
            this.method = root.getAttribute(METHOD_ATTRIBUTE);

            if (StringUtils.hasText(defaultCache)) {
                this.caches = StringUtils.commaDelimitedListToStringArray(defaultCache.trim());
            }
        }

        <T extends ReplayLockOperation.Builder> T merge(Element element, ReaderContext readerCtx, T builder) {
            String cache = element.getAttribute("cache");

            // sanity check
            String[] localCaches = this.caches;
            if (StringUtils.hasText(cache)) {
                localCaches = StringUtils.commaDelimitedListToStringArray(cache.trim());
            }
            if (localCaches != null) {
                builder.setCacheNames(localCaches);
            } else {
                readerCtx.error("No cache specified for " + element.getNodeName(), element);
            }

            builder.setKey(getAttributeValue(element, "key", this.key));
            builder.setKeyGenerator(getAttributeValue(element, "key-generator", this.keyGenerator));
            builder.setCacheManager(getAttributeValue(element, "cache-manager", this.cacheManager));
            builder.setCondition(getAttributeValue(element, "condition", this.condition));

            if (StringUtils.hasText(builder.getKey()) && StringUtils.hasText(builder.getKeyGenerator())) {
                throw new IllegalStateException("Invalid cache advice configuration on '" +
                        element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
                        "These attributes are mutually exclusive: either set the SpEL expression used to" +
                        "compute the key at runtime or set the name of the KeyGenerator bean to use.");
            }

            return builder;
        }

        @Nullable
        String merge(Element element, ReaderContext readerCtx) {
            String method = element.getAttribute(METHOD_ATTRIBUTE);
            if (StringUtils.hasText(method)) {
                return method.trim();
            }
            if (StringUtils.hasText(this.method)) {
                return this.method;
            }
            readerCtx.error("No method specified for " + element.getNodeName(), element);
            return null;
        }
    }

}
